Skip to content

frontend: video-manager: Rework thumbnails#3839

Merged
patrickelectric merged 4 commits intobluerobotics:1.4from
joaoantoniocardoso:1.4-rework-thumbnails
Mar 20, 2026
Merged

frontend: video-manager: Rework thumbnails#3839
patrickelectric merged 4 commits intobluerobotics:1.4from
joaoantoniocardoso:1.4-rework-thumbnails

Conversation

@joaoantoniocardoso
Copy link
Member

@joaoantoniocardoso joaoantoniocardoso commented Mar 19, 2026

Cherry-picks to 1.4:

  • b9b8fdd9 frontend: components: video-manager: VideoThumbnail: Debounce stop and fix blob URL cleanup
  • 23fcf198 frontend: store: video: Use HMR-safe singleton for thumbnail polling state
  • a5d29507 frontend: components: video-manager: VideoThumbnail: Add manual thumbnail controls
  • 34fc40b6 frontend: components: video-manager: VideoStreamCreationDialog: Add extended config booleans and fix form state reset

Summary by Sourcery

Rework video thumbnail fetching and controls, improve thumbnail polling state management, and extend video stream configuration options.

New Features:

  • Add manual controls to fetch single or continuous thumbnails in the video thumbnail component.
  • Show thumbnail fetching latency and state overlays in pirate mode for video thumbnails.
  • Expose extended stream configuration toggles for lazy mode, thumbnails, and Zenoh in the video stream creation dialog.

Bug Fixes:

  • Debounce stopping of thumbnail polling when unregistering a thumbnail to avoid premature backend stop calls.
  • Ensure video stream creation dialog form state is reset from the current stream each time the dialog is opened.
  • Avoid revoking active thumbnail blob URLs while they are still in use.

Enhancements:

  • Refine thumbnail placeholders and layout, including a disabled state for sources with thumbnails turned off.
  • Refactor thumbnail polling to use a hot-reload-safe singleton state shared across the app and guard concurrent fetch cycles.
  • Track and store thumbnail round-trip time in the video store for use in UI overlays.

…d fix blob URL cleanup

Debounce stopGetThumbnailForDevice by 15 seconds to ride through
transient backend restarts without interrupting thumbnail polling.
Move blob URL revocation to beforeDestroy to prevent premature cleanup.
…state

Move thumbnail sources, busy set, and OneMoreTime task to a
window-global singleton that persists across hot module reloads.
Add reentrant guard to prevent duplicate fetchThumbnails execution
from the double @module decorator. Reduce polling delay to 100ms
and preserve last valid thumbnail on 503 errors.
…nail controls

Default to paused thumbnail fetching to avoid unnecessary resource
usage. Add snapshot (single-fetch) and play/pause (continuous 1s
polling) controls. Show a warning that thumbnails use stream resources
and may affect video quality. Replace v-avatar with a 16:9 aspect
ratio frame for consistent placeholder and image dimensions.
@sourcery-ai
Copy link

sourcery-ai bot commented Mar 19, 2026

Reviewer's Guide

Reworks frontend video thumbnail handling by introducing manual snapshot/continuous controls with status/latency overlay, centralizing thumbnail polling into an HMR-safe singleton with better blob URL lifecycle, and extending the video stream creation/controls dialogs with new config flags and proper form state reset.

Sequence diagram for manual single-shot thumbnail fetch

sequenceDiagram
  actor User
  participant VideoControlsDialog
  participant VideoThumbnail
  participant VideoStore
  participant ThumbnailState as ThumbnailFetchState
  participant Scheduler as OneMoreTime_task
  participant Backend as BackendAPI

  User->>VideoControlsDialog: Open controls dialog
  VideoControlsDialog->>VideoThumbnail: render with register=true disabled=false

  User->>VideoThumbnail: Click single-shot button
  VideoThumbnail->>VideoThumbnail: fetchSingleThumbnail()
  VideoThumbnail->>VideoThumbnail: snapshot_in_progress = true
  VideoThumbnail->>VideoStore: startGetThumbnailForDevice(source)
  VideoStore->>ThumbnailState: sources.add(source)
  alt task paused or stopped
    VideoStore->>Scheduler: start or resume()
  end

  loop every 1s
    Scheduler->>VideoStore: fetchThumbnails()
    VideoStore->>ThumbnailState: check inProgress flag
    alt not inProgress
      VideoStore->>ThumbnailState: inProgress = true
      VideoStore->>Backend: GET /thumbnail?source=source
      Backend-->>VideoStore: image blob, status, latency
      VideoStore->>VideoStore: revoke old blob URL
      VideoStore->>VideoStore: set thumbnails[source] = {source,status,roundtripMs}
      VideoStore->>ThumbnailState: busy.delete(source)
      VideoStore->>ThumbnailState: inProgress = false
    else inProgress
      VideoStore-->>Scheduler: skip cycle
    end
  end

  participant UpdateTask as VideoThumbnail_update_task
  UpdateTask->>VideoThumbnail: updateThumbnail()
  VideoThumbnail->>VideoStore: thumbnails.get(source)
  VideoStore-->>VideoThumbnail: Thumbnail
  VideoThumbnail->>VideoThumbnail: load Image(result.source)
  VideoThumbnail->>VideoThumbnail: last_fetch_ms = result.roundtripMs
  VideoThumbnail->>VideoThumbnail: thumbnail = result
  alt snapshot_in_progress
    VideoThumbnail->>VideoThumbnail: snapshot_in_progress = false
    VideoThumbnail->>VideoThumbnail: snapshot_cooldown = true
    Note over VideoThumbnail: internal timeout 1s to clear cooldown
    VideoThumbnail->>VideoStore: stopGetThumbnailForDevice(source)
    VideoStore->>ThumbnailState: sources.delete(source)
    alt sources.size == 0
      VideoStore->>Scheduler: stop()
    end
  end
Loading

Sequence diagram for debounced unregister of continuous thumbnails

sequenceDiagram
  participant Parent as ParentComponent
  participant VideoThumbnail
  participant VideoStore
  participant ThumbnailState as ThumbnailFetchState
  participant Scheduler as OneMoreTime_task

  rect rgb(235, 245, 255)
    Parent->>VideoThumbnail: mount with register=true
    alt continuous_mode initially true
      VideoThumbnail->>VideoStore: startGetThumbnailForDevice(source)
      VideoStore->>ThumbnailState: sources.add(source)
      VideoStore->>Scheduler: start or resume()
    end
  end

  rect rgb(255, 245, 235)
    Parent->>VideoThumbnail: set register=false
    VideoThumbnail->>VideoThumbnail: watcher register(false,true)
    VideoThumbnail->>VideoThumbnail: clearTimeout(stopDebounceTimer)
    VideoThumbnail->>VideoThumbnail: stopDebounceTimer = setTimeout(15s)
  end

  Note over VideoThumbnail: during debounce window
  alt re-register before timeout
    Parent->>VideoThumbnail: set register=true
    VideoThumbnail->>VideoThumbnail: watcher register(true,false)
    VideoThumbnail->>VideoThumbnail: clearTimeout(stopDebounceTimer)
    VideoThumbnail->>VideoThumbnail: stopDebounceTimer = undefined
    alt continuous_mode
      VideoThumbnail->>VideoStore: startGetThumbnailForDevice(source)
    end
  else timeout fires
    VideoThumbnail->>VideoStore: stopGetThumbnailForDevice(source)
    VideoThumbnail->>VideoThumbnail: continuous_mode = false
    VideoStore->>ThumbnailState: sources.delete(source)
    alt sources empty
      VideoStore->>Scheduler: stop()
    end
  end

  rect rgb(245, 245, 245)
    VideoThumbnail->>VideoThumbnail: beforeDestroy()
    VideoThumbnail->>VideoThumbnail: clearTimeout(stopDebounceTimer)
    VideoThumbnail->>VideoStore: thumbnails.get(source)
    VideoStore-->>VideoThumbnail: Thumbnail
    alt blob URL exists
      VideoThumbnail->>VideoThumbnail: URL.revokeObjectURL(blobUrl)
    end
    VideoThumbnail->>VideoStore: stopGetThumbnailForDevice(source)
  end
Loading

Class diagram for updated video thumbnail handling and polling

classDiagram
  class VideoThumbnail {
    +String source
    +String width
    +Boolean register
    +Boolean disabled
    -- state --
    -Thumbnail thumbnail
    -OneMoreTime update_task
    -Number stopDebounceTimer
    -Boolean continuous_mode
    -Boolean snapshot_in_progress
    -Boolean snapshot_cooldown
    -Number last_fetch_ms
    -- computed --
    +Boolean not_available
    +Boolean fetching
    +Boolean idle_placeholder
    +Boolean is_pirate_mode
    -- methods --
    +updateThumbnail()
    +fetchSingleThumbnail()
    +toggleContinuous()
  }

  class Thumbnail {
    +String source
    +Number status
    +Number roundtripMs
  }

  class ThumbnailFetchState {
    +OneMoreTime task
    +Set~String~ sources
    +Set~String~ busy
    +Boolean inProgress
  }

  class VideoStore {
    +Map~String,Thumbnail~ thumbnails
    +Boolean updating_streams
    -- actions --
    +fetchThumbnails()
    +startGetThumbnailForDevice(source)
    +stopGetThumbnailForDevice(source)
    -- mutations --
    +setUpdatingStreams(updating)
  }

  class VideoControlsDialog {
    +Boolean thumbnailRegister
    +Boolean thumbnailDisabled
  }

  class VideoStreamCreationDialog {
    +Boolean show
    +String stream_name
    +String selected_encode
    +Object selected_size
    +Number selected_interval
    +Array~String~ stream_endpoints
    +Boolean is_thermal
    +Boolean is_disable_lazy
    +Boolean is_disable_mavlink
    +Boolean is_disable_thumbnails
    +Boolean is_disable_zenoh
    +resetFormFromStream()
  }

  class Settings {
    +Boolean is_pirate_mode
  }

  class BackAxios {
    +get(url,config)
  }

  %% Relationships
  VideoThumbnail --> VideoStore : uses video
  VideoThumbnail --> Thumbnail : displays
  VideoStore --> Thumbnail : stores
  VideoStore --> ThumbnailFetchState : uses singleton
  ThumbnailFetchState --> OneMoreTime : schedules
  VideoThumbnail --> Settings : reads
  VideoControlsDialog --> VideoThumbnail : renders
  VideoStreamCreationDialog --> VideoStore : configures streams
  VideoStore --> BackAxios : fetchThumbnails
Loading

File-Level Changes

Change Details Files
Add manual thumbnail controls, richer states, and latency overlay to VideoThumbnail while improving lifecycle handling.
  • Replace VAvatar-based thumbnail container with a custom 16:9 frame, new placeholder states for disabled/not-available/idle, and full-size image styling.
  • Introduce manual single-shot and continuous thumbnail controls, including pirate-mode overlay that shows LIVE/status and last fetch round-trip in ms.
  • Track thumbnail fetching state locally (continuous_mode, snapshot_in_progress, snapshot_cooldown, last_fetch_ms) and expose new disabled prop to control availability.
  • Debounce thumbnail stop logic when register toggles off, only stopping polling after a delay if not re-registered, and move start/stop calls to respect continuous mode.
  • Improve cleanup by revoking the previous blob URL on component destroy and when stopping snapshot fetches to avoid leaks.
core/frontend/src/components/video-manager/VideoThumbnail.vue
Centralize thumbnail polling state in a global HMR-safe singleton and augment thumbnail metadata with round-trip timing and better failure handling.
  • Define a shared ThumbnailFetchState (task, sources, busy, inProgress) on window under a stable key and wire fetchThumbnails to use it instead of module-local sets and task.
  • Guard fetchThumbnails with an inProgress flag and track per-request start time to compute roundtripMs, storing it in the Thumbnail object on success and SERVICE_UNAVAILABLE responses.
  • Ensure previous thumbnail blob URLs are revoked before replacing them, but no longer on stopGetThumbnailForDevice, to keep last image until replaced or component cleanup.
  • Update startGetThumbnailForDevice / stopGetThumbnailForDevice to add/remove sources from the global set and start/resume/stop the shared OneMoreTime polling task appropriately.
  • Wire the shared polling task’s action to video.fetchThumbnails instead of the removed module-level fetchThumbnailsTask.
core/frontend/src/store/video.ts
Extend video stream creation dialog with new extended configuration flags and ensure form state resets from the current stream whenever reopened.
  • Add new extended configuration checkboxes (Disable Lazy, Disable Thumbnails, Disable Zenoh) bound to corresponding data properties.
  • Include disable_lazy, disable_thumbnails, and disable_zenoh in both the default stream shape and the payload’s extended_configuration when creating/updating streams.
  • Initialize dialog data properties from the bound stream’s extended configuration so existing streams reflect their current flags in the UI.
  • Introduce a show watcher that calls a new resetFormFromStream() helper to fully rehydrate form fields (name, encode, size, interval, endpoints, extended flags) from the current stream whenever the dialog is opened.
core/frontend/src/components/video-manager/VideoStreamCreationDialog.vue
Propagate thumbnail disable capability into the video controls dialog and align with new VideoThumbnail API.
  • Update VideoControlsDialog usage of VideoThumbnail to drop the height prop, pass through new thumbnailDisabled prop, and keep width/register/source.
  • Add thumbnailDisabled as a Boolean prop on VideoControlsDialog to allow callers to control whether thumbnails are enabled for the device.
core/frontend/src/components/video-manager/VideoControlsDialog.vue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

…xtended config booleans and fix form state reset

Add disable_lazy, disable_thumbnails, and disable_zenoh checkboxes
to the extra configuration panel. Fix stale form state on dialog
reopen by re-syncing all fields from the stream prop via a watcher.
@joaoantoniocardoso joaoantoniocardoso marked this pull request as ready for review March 19, 2026 21:30
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In VideoThumbnail.updateThumbnail, failures to load the image or non-200 thumbnail responses never clear snapshot_in_progress or update last_fetch_ms, which can leave the UI stuck in a fetching state; consider handling img.onerror and non-200 statuses to reset the snapshot flags and surface the latest status/latency.
  • The HMR-safe thumbnail polling singleton relies on OneMoreTime internals (isPaused, isRunning, timeoutId) via as any; it would be more robust to rely only on public APIs or track the task state in your own thumbnailState flags to avoid breakage if OneMoreTime changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `VideoThumbnail.updateThumbnail`, failures to load the image or non-200 thumbnail responses never clear `snapshot_in_progress` or update `last_fetch_ms`, which can leave the UI stuck in a fetching state; consider handling `img.onerror` and non-200 statuses to reset the snapshot flags and surface the latest status/latency.
- The HMR-safe thumbnail polling singleton relies on `OneMoreTime` internals (`isPaused`, `isRunning`, `timeoutId`) via `as any`; it would be more robust to rely only on public APIs or track the task state in your own `thumbnailState` flags to avoid breakage if `OneMoreTime` changes.

## Individual Comments

### Comment 1
<location path="core/frontend/src/components/video-manager/VideoThumbnail.vue" line_range="186-187" />
<code_context>
   },
   beforeDestroy() {
+    clearTimeout(this.stopDebounceTimer)
+    const blobUrl = video.thumbnails.get(this.source)?.source
+    if (blobUrl !== undefined) {
+      URL.revokeObjectURL(blobUrl)
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** Revoking the shared blob URL in the component can break other thumbnails using the same source.

Because the store shares `video.thumbnails` entries across `VideoThumbnail` instances for the same `source`, revoking the URL in `beforeDestroy` will invalidate it for any other component still using it. Since `fetchThumbnails` already revokes old URLs when replacing them, this per-component `URL.revokeObjectURL` should likely be removed and the store should own blob URL lifecycle centrally.
</issue_to_address>

### Comment 2
<location path="core/frontend/src/components/video-manager/VideoThumbnail.vue" line_range="3-12" />
<code_context>
         :src="thumbnail?.source"
+        style="width: 100%; height: 100%; object-fit: cover;"
+      >
+      <div
+        v-if="is_pirate_mode && register"
+        class="thumbnail-overlay text-caption"
+      >
+        <template v-if="continuous_mode">
+          LIVE
+          <template v-if="last_fetch_ms !== null">
+            {{ last_fetch_ms }}ms
+          </template>
+        </template>
+        <template v-else-if="snapshot_in_progress">
+          fetching...
+        </template>
+        <template v-else-if="thumbnail">
+          <template v-if="last_fetch_ms !== null">
+            {{ last_fetch_ms }}ms
+          </template>
+        </template>
+        <template v-else>
+          idle
+        </template>
+      </div>
+      <div
+        v-if="register"
+        class="thumbnail-controls d-flex align-center"
       >
</code_context>
<issue_to_address>
**issue (bug_risk):** Thumbnail controls are still active when thumbnails are disabled.

When `disabled` is true, the placeholder shows correctly, but the controls still render whenever `register` is true and still invoke `fetchSingleThumbnail` / `toggleContinuous`. This lets users trigger thumbnail fetching despite the disabled state. Please gate the controls on `!disabled` (e.g. `v-if="register && !disabled"`) or add a runtime guard in the click handlers so that `disabled` fully prevents thumbnail fetching.
</issue_to_address>

### Comment 3
<location path="core/frontend/src/store/video.ts" line_range="270-271" />
<code_context>
-    } else {
-      this.fetchThumbnailsTask.start()
+    thumbnailState.sources.add(source)
+    const task = thumbnailState.task as any
+    if (task.isPaused) {
+      thumbnailState.task.resume()
+    } else if (!task.isRunning && !task.timeoutId) {
</code_context>
<issue_to_address>
**suggestion:** Accessing internal `OneMoreTime` fields makes thumbnail scheduling brittle.

This relies on non-public `OneMoreTime` fields (`isPaused`, `isRunning`, `timeoutId`) via `as any`, so it will break if `OneMoreTime`’s internals change. Prefer using only the public API—for example, track running/paused state in `thumbnailState`, or extend `OneMoreTime` with proper accessors—instead of reaching into internal fields.

Suggested implementation:

```typescript
  // eslint-disable-next-line class-methods-use-this
  @Action
  startGetThumbnailForDevice(source: string): void {
    thumbnailState.sources.add(source)

    const { task } = thumbnailState

    if (thumbnailState.isPaused) {
      task.resume()
      thumbnailState.isPaused = false
      thumbnailState.isRunning = true
    } else if (!thumbnailState.isRunning) {
      task.start()
      thumbnailState.isRunning = true
    }
  }

```

To fully remove the dependency on internal `OneMoreTime` fields and make this robust:

1. Extend `thumbnailState` to track the task state explicitly, for example:
   - Initialize `isRunning: boolean` and `isPaused: boolean` in the state definition for thumbnails.
2. Wherever `thumbnailState.task.start()`, `thumbnailState.task.pause()`, `thumbnailState.task.resume()`, or `thumbnailState.task.stop()` are called elsewhere in `video.ts` (or related modules), update `thumbnailState.isRunning` / `thumbnailState.isPaused` accordingly, e.g.:
   - After `task.start()`: `isRunning = true`, `isPaused = false`
   - After `task.pause()`: `isRunning = false`, `isPaused = true`
   - After `task.resume()`: `isRunning = true`, `isPaused = false`
   - After `task.stop()`: `isRunning = false`, `isPaused = false`
3. Ensure `thumbnailState.isRunning` and `thumbnailState.isPaused` are correctly initialized when the store/module is created (e.g. both `false`).
4. Remove any remaining `as any` casts and direct accesses to `OneMoreTime` internals (`isPaused`, `isRunning`, `timeoutId`) elsewhere in the codebase, replacing them with checks against the new `thumbnailState` flags.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +186 to +187
const blobUrl = video.thumbnails.get(this.source)?.source
if (blobUrl !== undefined) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Revoking the shared blob URL in the component can break other thumbnails using the same source.

Because the store shares video.thumbnails entries across VideoThumbnail instances for the same source, revoking the URL in beforeDestroy will invalidate it for any other component still using it. Since fetchThumbnails already revokes old URLs when replacing them, this per-component URL.revokeObjectURL should likely be removed and the store should own blob URL lifecycle centrally.

Comment on lines +3 to +12
<div
class="thumbnail-frame d-flex align-center justify-center"
:style="{ width: width + 'px', aspectRatio: '16 / 9' }"
>
<span
v-if="not_available"
class="text-caption"
style="border: 2px dashed; opacity: 30%; padding: 20px;"
v-if="disabled"
class="text-caption text-center placeholder-box"
>
Preview not<br>
available<br>
Add a stream<br>
to have one
Thumbnails disabled<br>
for this source.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Thumbnail controls are still active when thumbnails are disabled.

When disabled is true, the placeholder shows correctly, but the controls still render whenever register is true and still invoke fetchSingleThumbnail / toggleContinuous. This lets users trigger thumbnail fetching despite the disabled state. Please gate the controls on !disabled (e.g. v-if="register && !disabled") or add a runtime guard in the click handlers so that disabled fully prevents thumbnail fetching.

Comment on lines +270 to +271
const task = thumbnailState.task as any
if (task.isPaused) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Accessing internal OneMoreTime fields makes thumbnail scheduling brittle.

This relies on non-public OneMoreTime fields (isPaused, isRunning, timeoutId) via as any, so it will break if OneMoreTime’s internals change. Prefer using only the public API—for example, track running/paused state in thumbnailState, or extend OneMoreTime with proper accessors—instead of reaching into internal fields.

Suggested implementation:

  // eslint-disable-next-line class-methods-use-this
  @Action
  startGetThumbnailForDevice(source: string): void {
    thumbnailState.sources.add(source)

    const { task } = thumbnailState

    if (thumbnailState.isPaused) {
      task.resume()
      thumbnailState.isPaused = false
      thumbnailState.isRunning = true
    } else if (!thumbnailState.isRunning) {
      task.start()
      thumbnailState.isRunning = true
    }
  }

To fully remove the dependency on internal OneMoreTime fields and make this robust:

  1. Extend thumbnailState to track the task state explicitly, for example:
    • Initialize isRunning: boolean and isPaused: boolean in the state definition for thumbnails.
  2. Wherever thumbnailState.task.start(), thumbnailState.task.pause(), thumbnailState.task.resume(), or thumbnailState.task.stop() are called elsewhere in video.ts (or related modules), update thumbnailState.isRunning / thumbnailState.isPaused accordingly, e.g.:
    • After task.start(): isRunning = true, isPaused = false
    • After task.pause(): isRunning = false, isPaused = true
    • After task.resume(): isRunning = true, isPaused = false
    • After task.stop(): isRunning = false, isPaused = false
  3. Ensure thumbnailState.isRunning and thumbnailState.isPaused are correctly initialized when the store/module is created (e.g. both false).
  4. Remove any remaining as any casts and direct accesses to OneMoreTime internals (isPaused, isRunning, timeoutId) elsewhere in the codebase, replacing them with checks against the new thumbnailState flags.

@patrickelectric patrickelectric merged commit f353eab into bluerobotics:1.4 Mar 20, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants